// Copyright (c) 2019 Presto Labs Pte. Ltd.
// Author: donggu

#include <cstdint>
#include <ctime>

// http://boost.sourceforge.net/regression-logs/cs-win32_metacomm/doc/html/date_time.doxy.html
#include <chrono>
#include <iostream>
#include <regex>
#include <sstream>

#include <boost/date_time/gregorian/gregorian_types.hpp>
#include <boost/date_time/posix_time/posix_time_types.hpp>
#include <boost/date_time/posix_time/time_formatters.hpp>
#include <fmt/core.h>

#include "coin2/base/log.h"
#include "coin2/base/time.h"

namespace impl {

using boost::gregorian::date;
using boost::posix_time::nanoseconds;
using boost::posix_time::ptime;
using boost::posix_time::time_duration;

std::string Iso8601FromTimestamp(int64_t ts) {
  // 2019-11-02T10:12:21.0000Z
  auto p = PtimeFromTimestamp(ts);
  return boost::posix_time::to_iso_extended_string(p);
}

std::string YmdFromTimestamp(int64_t ts) {
  auto p = PtimeFromTimestamp(ts);
  return fmt::format("{}{:0>2}{:0>2}", p.date().year(), p.date().month(), p.date().day());
}

ptime PtimeFromDatetime(
    int year,
    int month,
    int day,
    int hour,
    int minute,
    int second,
    int64_t nano) {
  return ptime(date(year, month, day), time_duration(hour, minute, second) + nanoseconds(nano));
}

ptime PtimeFromTimestamp(int64_t timestamp) {
  return ptime(date(1970, 1, 1), nanoseconds(timestamp));
}

static const date epoch = date(1970, 1, 1);

int64_t TimestampToUtc0(int64_t timestamp) {
  ptime pt = PtimeFromTimestamp(timestamp);
  ptime midnight = ptime(pt.date(), time_duration(0, 0, 0));
  return (midnight - ptime(epoch)).total_nanoseconds();
}

// 2019-11-02T10:12:21Z
// 2019-02-03T00:33:29.410Z
ptime PtimeFromIso8601(const std::string& str) {
  int y;
  int M = 1;
  int d = 1;
  int h = 0;
  int m = 0;
  int s = 0;
  char nano[11];  // 9 digits + Z + null

  int ret;
  if (str.find_first_of("-:") == std::string::npos) {
    // 2019
    // 201911
    // 20191102
    // 20191102T10
    // 20191102T1012
    // 20191102T101221
    // 20191102T101221Z
    // 20191102T101221.0000
    // 20191102T101221.0000Z
    ret = sscanf(str.c_str(), "%4d%2d%2dT%2d%2d%2d.%10s", &y, &M, &d, &h, &m, &s, nano);
  } else {
    // 2019
    // 2019-11
    // 2019-11-02
    // 2019-11-02T10
    // 2019-11-02T10:12
    // 2019-11-02T10:12:21
    // 2019-11-02T10:12:21Z
    // 2019-11-02T10:12:21.0000
    // 2019-11-02T10:12:21.0000Z
    ret = sscanf(str.c_str(), "%4d-%2d-%2dT%2d:%2d:%2d.%10s", &y, &M, &d, &h, &m, &s, nano);
  }

  CHECK_GE_THROW(ret, 1) << "Cannot parse iso8601: " << str;

  int64_t n = 0;
  if (ret == 7) {
    // .123 = 123'000'000 ns
    // .1234 = 123'400'000 ns
    int i;
    for (i = 0; i < 9 && nano[i] && nano[i] != 'Z'; i++) {
      n = n * 10 + nano[i] - '0';
    }
    CHECK(nano[i] == 'Z') << "[time.h] iso string doesn't end with Z: " << nano;
    for (; i < 9; i++) {
      n = n * 10;
    }
  }

  return PtimeFromDatetime(y, M, d, h, m, s, n);
}

int64_t TimestampFromPtime(ptime time) { return (time - ptime(epoch)).total_nanoseconds(); }

int64_t TimestampFromIso8601(const std::string& str) {
  return TimestampFromPtime(PtimeFromIso8601(str));
}

#define LIKELY(x) __builtin_expect(!!(x), 1)
#define UNLIKELY(x) __builtin_expect(!!(x), 0)

// okex confirmed that their timestamp is 1 format
// "timestamp": "2019-05-06T07:19:39.348Z"
ptime PtimeFromOkexIso8601(std::string_view view) {
  const char* cur = view.data();
  CHECK_GE_THROW(view.size(), 20) << view;
  uint16_t year = 0, month = 0, day = 0, hour = 0, min = 0, sec = 0, msec = 0;
  sscanf(cur, "%4hu-%4hu-%4huT%4hu:%4hu:%4hu.%4huZ", &year, &month, &day, &hour, &min, &sec, &msec);

  if (UNLIKELY(
          year < 1 || month < 1 || month > 12 || day < 1 || day > 31 || hour > 23 || min > 59 ||
          sec > 59 || msec > 999)) {
    THROW() << view;
  }
  return PtimeFromDatetime(year, month, day, hour, min, sec, msec * 1e6);
}

// Okex Index feed use different timestamp format, eg:
// 2021-01-26T09:43:52.464712794Z
ptime PtimeFromOkexIndexIso8601(std::string_view view) {
  const char* cur = view.data();
  CHECK_GE_THROW(view.size(), 20) << view;
  uint16_t year = 0, month = 0, day = 0, hour = 0, min = 0, sec = 0;
  uint32_t nanosec = 0;
  sscanf(cur, "%4hu-%4hu-%4huT%4hu:%4hu:%4hu.%9duZ",
         &year, &month, &day, &hour, &min, &sec, &nanosec);

  if (UNLIKELY(
          year < 1 || month < 1 || month > 12 || day < 1 || day > 31 || hour > 23 || min > 59 ||
          sec > 59 || nanosec > 999999999)) {
    THROW() << view;
  }
  if (UNLIKELY(nanosec < 1000)) {
    nanosec = nanosec * 1e6;
  }
  return PtimeFromDatetime(year, month, day, hour, min, sec, nanosec);
}

int64_t TimestampFromOkexIso8601(std::string_view view) {
  return TimestampFromPtime(PtimeFromOkexIso8601(view));
}

int64_t TimestampFromOkexIndexIso8601(std::string_view view) {
  return TimestampFromPtime(PtimeFromOkexIndexIso8601(view));
}

//"time": "2022-09-02T07:35:53.000+00:00"
int64_t TimestampFromMexcIso8601(std::string_view view) {
  const char* cur = view.data();
  uint16_t year, month, day, hour, min, sec;
  uint32_t usec;
  if (LIKELY(view.size() >= 29)) {
    sscanf(cur, "%4hu-%4hu-%4huT%4hu:%4hu:%4hu.%3u+00:00", &year, &month, &day, &hour, &min, &sec, &usec);
  } else {
    THROW() << "Can't parse: " << view;
    // for -Wsometimes-uninitialized
    year = month = day = hour = min = sec = 0;
    usec = 0;
  }
  if (UNLIKELY(
          year < 1 || month < 1 || month > 12 || day < 1 || day > 31 || hour > 23 || min > 59 ||
          sec > 59 || usec > 999999)) {
    THROW() << view;
  }
  return TimestampFromPtime(PtimeFromDatetime(year, month, day, hour, min, sec, usec * 1e6));
}

// "time": "2020-05-12T07:05:31.232439+00:00"
ptime PtimeFromFtxIso8601(std::string_view view) {
  const char* cur = view.data();
  uint16_t year, month, day, hour, min, sec;
  uint32_t usec;
  if (LIKELY(view.size() >= 32)) {
    sscanf(
        cur,
        "%4hu-%4hu-%4huT%4hu:%4hu:%4hu.%6u+00:00",
        &year,
        &month,
        &day,
        &hour,
        &min,
        &sec,
        &usec);
  } else if (view.size() == 25) {
    sscanf(
        cur,
        "%4hu-%4hu-%4huT%4hu:%4hu:%4hu+00:00",
        &year,
        &month,
        &day,
        &hour,
        &min,
        &sec);
    usec = 0;
  } else {
    THROW() << "Can't parse: " << view;
    // for -Wsometimes-uninitialized
    year = month = day = hour = min = sec = 0;
    usec = 0;
  }

  if (UNLIKELY(
          year < 1 || month < 1 || month > 12 || day < 1 || day > 31 || hour > 23 || min > 59 ||
          sec > 59 || usec > 999999)) {
    THROW() << view;
  }
  return PtimeFromDatetime(year, month, day, hour, min, sec, usec * 1e3);
}

int64_t TimestampFromFtxIso8601(std::string_view view) {
  return TimestampFromPtime(PtimeFromFtxIso8601(view));
}

// "time": "2020-05-12 07:05:31.232439+00"
ptime PtimeFromGdaxIso8601(std::string_view view) {
  const char* cur = view.data();
  uint16_t year, month, day, hour, min, sec;
  uint32_t usec;
  if (LIKELY((view.size() < 30) && (view.size() > 23))) {
    char format_str[64] = {0};
    sprintf(format_str, "%%4hu-%%4hu-%%4hu %%4hu:%%4hu:%%4hu.%%%du+00",
        (int)(view.size() - 23));
    sscanf(cur, format_str, &year, &month, &day, &hour, &min, &sec, &usec);
  } else if (view.size() > 21) {
    sscanf(cur, "%4hu-%4hu-%4huT%4hu:%4hu:%4hu+00", &year, &month, &day, &hour, &min, &sec);
    usec = 0;
  } else {
    THROW() << "Can't parse: " << view;
    // for -Wsometimes-uninitialized
    year = month = day = hour = min = sec = 0;
    usec = 0;
  }

  if (UNLIKELY(
          year < 1 || month < 1 || month > 12 || day < 1 || day > 31 || hour > 23 || min > 59 ||
          sec > 59 || usec > 999999)) {
    THROW() << view;
  }
  return PtimeFromDatetime(year, month, day, hour, min, sec, usec * 1e3);
}

int64_t TimestampFromGdaxIso8601(std::string_view view) {
  return TimestampFromPtime(PtimeFromGdaxIso8601(view));
}

int64_t TimestampFromString(const std::string& str) {
  if (str.size() == 19) {
    auto ts = std::strtoll(str.c_str(), nullptr, 10);  // 0 on failure
    if (ts) {
      return ts;
    }
  }
  return TimestampFromPtime(PtimeFromIso8601(str));
}

int64_t TimestampFromDatetime(
    int year,
    int month,
    int day,
    int hour,
    int minute,
    int second,
    int64_t nano) {
  return TimestampFromPtime(PtimeFromDatetime(year, month, day, hour, minute, second, nano));
}

// modified iso 8601
int64_t DurationFromIso8601(const std::string& s) {
  // P{00}DT{00}H{00}M{00}S{00}m{00}u{00}n
  std::regex DURATION(
      "P(([0-9]+)D)?(T(([0-9]+)H)?(([0-9]+)M)?(([0-9]+)S)?(([0-9]+)m)?(([0-9]+)u)?(([0-9]+)n)?)?");
  std::smatch matches;

  CHECK_THROW(std::regex_match(s, matches, DURATION)) << "invalid iso8601 duration: " << s;

  int64_t d = 0;
  if (matches[2].length()) {
    d += std::strtoll(matches[2].str().c_str(), nullptr, 10);  // day
  }
  d *= 24;  // day to hours
  if (matches[5].length()) {
    d += std::strtoll(matches[5].str().c_str(), nullptr, 10);  // hour
  }
  d *= 60;  // hour to min
  if (matches[7].length()) {
    d += std::strtoll(matches[7].str().c_str(), nullptr, 10);  // min
  }
  d *= 60;  // min to sec
  if (matches[9].length()) {
    d += std::strtoll(matches[9].str().c_str(), nullptr, 10);  // sec
  }
  d *= 1000;  // sec to milli
  if (matches[11].length()) {
    d += std::strtoll(matches[11].str().c_str(), nullptr, 10);  // ms
  }
  d *= 1000;  // milli to micro
  if (matches[13].length()) {
    d += std::strtoll(matches[13].str().c_str(), nullptr, 10);  // us
  }
  d *= 1000;  // micro to nano
  if (matches[15].length()) {
    d += std::strtoll(matches[15].str().c_str(), nullptr, 10);  // nano
  }

  return d;
}

int64_t DurationFromString(const std::string& s) {
  if (!s.empty() && s[0] == 'P') {
    return DurationFromIso8601(s);
  }
  return std::strtoll(s.c_str(), nullptr, 10);
}

std::pair<int64_t, int64_t> IntervalFromIso8601(const std::string& s) {
  size_t idx = s.find('/');
  CHECK_NE_THROW(idx, std::string::npos) << "iso8601 interval should have a slash";
  CHECK_GT_THROW(idx, 0) << "not iso8601";
  CHECK_GT_THROW(s.size() - idx - 1, 0) << "not iso8601";

  int64_t start;
  int64_t end;
  // <duration>/<end>
  if (s[0] == 'P') {
    int64_t d = DurationFromIso8601(s.substr(0, idx));
    end = TimestampFromIso8601(s.substr(idx + 1, s.size() - idx - 1));
    start = end - d;
  } else if (s[idx + 1] == 'P') {  // <start>/<duration>
    start = TimestampFromIso8601(s.substr(0, idx));
    int64_t d = DurationFromIso8601(s.substr(idx + 1, s.size() - idx - 1));
    end = start + d;
  } else {  // <start> / <end>
    start = TimestampFromIso8601(s.substr(0, idx));
    end = TimestampFromIso8601(s.substr(idx + 1, s.size() - idx - 1));
  }
  return std::make_pair(start, end);
}

std::pair<int64_t, int64_t> RateFromIso8601(const std::string& s) {
  // "240/P5H"
  auto idx = s.find("/");
  CHECK_NE_THROW(idx, std::string::npos) << "Invalid rate string: " << s;
  CHECK_GT_THROW(idx, 0) << "Invalid rate string: " << s;
  CHECK_LT_THROW(idx, s.size() - 1) << "Invalid rate string: " << s;
  return std::make_pair(
      std::strtoll(s.substr(0, idx).c_str(), nullptr, 10),
      DurationFromIso8601(s.substr(idx + 1, s.size() - idx - 1)));
}

// Fri < hour => Fri hour
// Fri >= hour => next Fri hour

int64_t Weekly(int64_t t, int hour) {
  // 0=mon, 4=fri, 6=sun
  int64_t time = t % (24 * 60 * 60 * 1'000'000'000ll);
  // >=hour? next day
  if (time >= hour * 60 * 60 * 1'000'000'000ll) {
    t = t + 24 * 60 * 60 * 1'000'000'000ll;
  }
  t = t - time + hour * 60 * 60 * 1'000'000'000ll;  // to hour

  int weekday = (3 + t / (24 * 60 * 60 * 1'000'000'000ll)) % 7;
  return t + (4 + 7 - weekday) % 7 * (24 * 60 * 60 * 1'000'000'000ll);
}
int64_t Biweekly(int64_t t) {
  THROW() << "biweekly() not implemented";
  return -1;
}
int64_t Quarterly(int64_t t, int hour) {
  // closest friday hour
  t = Weekly(t, hour);

  auto ptime = PtimeFromTimestamp(t);
  while (ptime.date().month() % 3) {               // not 3, 6, 9, 12?
    ptime += boost::gregorian::date_duration(28);  // add 4 weeks
    t += 28 * 24 * 60 * 60 * 1'000'000'000ll;
  }
  int weeks = (ptime.date().end_of_month().day() - ptime.date().day()) / 7;
  t += weeks * 7 * 24 * 60 * 60 * 1'000'000'000ll;
  return t;
}

std::string DebugStringFromDuration(int64_t duration) {
  std::stringstream ss;
  int64_t fraction = duration % 1'000'000'000;

  duration /= 1'000'000'000;
  int secs = duration % 60;
  duration /= 60;
  int mins = duration % 60;
  duration /= 60;
  int hours = duration % 24;
  int days = duration / 24;

  if (days) {
    ss << days << " days ";
  }
  if (hours) {
    ss << hours << " hours ";
  }
  if (mins) {
    ss << mins << " mins ";
  }
  if (secs) {
    ss << secs << " secs ";
  }

  if (fraction >= 10 * 1'000'000) {
    ss << " " << double(fraction) / 1'000'000 << " ms";
  } else if (fraction) {
    ss << " " << fraction / 1'000 << " us";
  }
  return ss.str();
}

}  // namespace impl
